/**
 * 
 */
//package assignment1;

import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.NoSuchElementException;
import java.util.Scanner;


/**
 * @author LN
 *
 */
class P2PNode {
	
	
	private int _fileID;
	private String _myAddr;			
	private int _mySharedPort;
	private String _initHost;
	private int _hostPort;
	private boolean _isInitHost;
	
	
	private State _state;	
	private Socket _socket = null;	
	private String _leaderFullAddr;
	private String _leaderHost;
	private int _leaderPort;
	
	/**
	 * List of files to share.
	 */
	private ArrayList<String> filelist = new ArrayList<String>();
	
	
	public int _leaderID;
	
	public int MyID;
	public int MyPort;
	
	public String MyFullAddr;
	public boolean _foundLeader = false;	
	public ServerSocket InitServer = null;	
	public Hashtable<Integer, String> ConnTable = new Hashtable<Integer, String>();	
	public Hashtable<String, String> FileShare = new Hashtable<String, String>();
	
	public enum State { HostListen, ClientWait, HostElectLeader, ConnectToLeader, 
						ClientShareFile, DisplayFiles, QueryFile, Idle };
	
		
	/**
	 * Creates init host.
	 */
	public P2PNode(int fileID, int ID, int port) {
		_fileID = fileID;	
		MyID = ID;
		MyPort = port;
		_mySharedPort = MyPort+5;
		
		_isInitHost = true;
		_state = State.HostListen;
					
		InetAddress addr;
		try {
			addr = InetAddress.getLocalHost();
			_myAddr = addr.getHostAddress();
			MyFullAddr = _myAddr + ":" + Integer.toString(MyPort);
						
			System.out.println("My ID: " + MyID);
			System.out.println("My IP: " + _myAddr);
			System.out.println("Accepts connections at port: " + MyPort);						
		} catch (UnknownHostException e) {
			System.out.println(e);
		} 
//		catch (IOException e) {
//			System.out.println(e);
//			System.out.println("Could not listen on port " + MyPort);
//			System.exit(-1);
//		} 
		String sharefile;
		sharefile = Integer.toString( 3*(_fileID-1)+1 );
		//System.out.println(sharefile);
		filelist.add(sharefile);
		sharefile = Integer.toString( 3*(_fileID-1)+2 );
		//System.out.println(sharefile);
		filelist.add(sharefile);
		sharefile = Integer.toString( 3*(_fileID-1)+3 );
		//System.out.println(sharefile);
		filelist.add(sharefile);
	}
	
	/**
	 * Creates init client.
	 */
	public P2PNode(int fileID, int ID, int port, String initHost, int hostPort) {
		
		this(fileID, ID, port);
		_initHost = initHost;
		_hostPort = hostPort;
		_isInitHost = false;
		_state = State.ClientWait;
	}
	
	public void launch() {
		
		//
		// Launch server for file transfer on new thread
		P2PServer p2pServer = new P2PServer(_mySharedPort);
		Thread fileTransferThread = new Thread(p2pServer);
		fileTransferThread.start();	
		
		while (true) {
		
			switch (_state) {			

			case HostListen:
				runHostListen();
				_state = State.HostElectLeader;
				break;
			case HostElectLeader:
				_leaderID = hostFindLeader();
				_leaderFullAddr = ConnTable.get(_leaderID);
				_foundLeader = true;
				_state = State.ConnectToLeader;		
				try {
					InitServer.close();
					System.out.println("Socket close.");
				}
				catch (IOException ex) {
					System.out.println("Could not close initial host server.");
			        System.exit(-1);
				}
				break;
			case ClientWait:
				runClientWait();
				_state = State.ConnectToLeader;
				break;
			case ConnectToLeader:
				connectToLeader();
				break;
			case ClientShareFile:
				sendFileshare();
				break;
			case DisplayFiles:
				displayFiles();			
				_state = State.Idle;
				break;
			case QueryFile:
				System.out.println("Client starts query.");
				clientQueryFile();
				break;	
			case Idle:
				try {
					Thread.sleep(2000);
					
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				break;
			}			
		}				
	}


	/**
	 * 
	 */
	private synchronized void displayFiles() {
		
		while (FileShare.size() < 6) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				System.out.println(e);
			}
		}
		
		Enumeration<String> filenames = FileShare.keys();
		String file, addr;
		System.out.println("Files for sharing:");
		while (filenames.hasMoreElements()) {
			file = filenames.nextElement();
			addr = FileShare.get(file);
			System.out.println(file + "\t" + addr);
		}
	}

	/**
	 * 
	 */
	private void clientQueryFile() {
		
		DataOutputStream _dataOut = null;
		DataInputStream _dataIn = null;
		
		try {		
			_dataIn = new DataInputStream(_socket.getInputStream());		// deprecated readLine()
			_dataOut = new DataOutputStream(_socket.getOutputStream());			
		}
		catch (IOException ex) {
			System.out.println("getInput/OutputStream() failed.");
			System.out.println(ex);
		    System.exit(-1);
		}
		
		Scanner scanner = new Scanner(System.in);
		boolean quit=false;
		while (!quit) {			
			String fileRequest=null;
			try {				
				System.out.print("File to search: ");
				fileRequest=scanner.nextLine();	// blocks until hit enter
				
				if (fileRequest != null) {
					try {
						_dataOut.writeUTF(fileRequest);
//						_dataOut.writeUTF(line + "\r\n");						
						_dataOut.flush();
						
					}
					catch (IOException e) {
						System.out.println(e);
					}
					System.out.println("Query for " + fileRequest + " has been sent to server.");
				}
				
				// wait till data is available from server
				while (_dataIn.available() <= 0) {
					try {
						Thread.sleep(50);
					} catch (InterruptedException e) {
						System.out.println(e);
					}													
				}
				boolean foundFile;
				String filename, fileAddr;
				
				/* receive response from server */
				while (_dataIn.available() > 0) {
					System.out.println("waiting for search results ...");
					foundFile = _dataIn.readBoolean();
					
					if (foundFile) {
						
						filename = _dataIn.readUTF();
						fileAddr = _dataIn.readUTF();
						System.out.println("Found file.");
						System.out.println(filename + "\t" + fileAddr);
						downloadFile(filename, fileAddr);
					}
					else {
						System.out.println("File not found.");
					}
				}
				
			} catch (NoSuchElementException ex) {
				// empty input, wait for user input
				System.out.println("empty line.");
				
			} catch (IOException e) {
				System.out.println("Could not read from server.");
			}
		}		
	}

	/**
	 * Downloads file.
	 */
	private void downloadFile(String filename, String hostAddr) {
					
		DataOutputStream _dataOut = null;
		int bufferSize = 4096;	    	          

	    String[] temp = hostAddr.split(":");
	    
	    // make a new connection to server and download file.
	    Socket sock;
		try {
			System.out.println("Connecting to download server...");
			sock = new Socket(temp[0], Integer.parseInt(temp[1]));			
			System.out.println("Connection to file server established !");
			
			String folder = createFolder(MyID);
			System.out.println("Download to: " + folder);
			
			// send filename to host
			System.out.println("Sending file request to server...");
		    _dataOut = new DataOutputStream(sock.getOutputStream());
		    _dataOut.writeUTF(filename);
			_dataOut.flush();
		    
			// byte array contains downloaded file
		    byte [] buffer  = new byte [ bufferSize ];
		    int readSize; 
            int totalSize = 0;
            
		    // get the socket's input stream 
            InputStream clientInputStream = sock.getInputStream();
		    FileOutputStream fileOutStream;
            BufferedOutputStream bufferedOutStr;
            
            // create a buffered output stream to write file to local disk
            fileOutStream = new FileOutputStream( new File( folder + filename ) );                       
            bufferedOutStr = new BufferedOutputStream(fileOutStream);
		    		    		
            while ((readSize = clientInputStream.read(buffer)) != -1) {
            	System.out.println("Downloading....");
            	totalSize += readSize;
            	bufferedOutStr.write(buffer, 0, readSize);
            }            
            bufferedOutStr.flush();			// flush data to disk, VERY IMPORTANT
            
            System.out.println("File " + filename + " has been downloaded.");
                        
            // Close streams after each download
            fileOutStream.close();
            bufferedOutStr.close();
            clientInputStream.close();
		    sock.close();
		    
		} catch (NumberFormatException e) {
			System.out.println(e);
		} catch (UnknownHostException e) {
			System.out.println(e);
		} catch (IOException e) {
			System.out.println(e);
		}	    	    	    	    	    	    
	}
	
	/**
	 * Creates "download#" folder if not existed.
	 * Returns path to this folder.
	 */
	private String createFolder(int unique) {
		
		String downFolder = "download" + unique;
		String currentDir="";
		try {
			currentDir = new File(".").getCanonicalPath();
			downFolder = currentDir + File.separator + downFolder;
		} catch (IOException e) {
			e.printStackTrace();
		}
			
		File dir = new File(downFolder);
		
		if (!dir.exists()) {
			if (!dir.mkdir()) {
				System.out.println("Could not create \"" + downFolder +"\"");
				return currentDir;
			}			
		}
		return downFolder + File.separator;		
	}

	/**
	 * 
	 */
	private void connectToLeader() {

		String[] temp = _leaderFullAddr.split(":");
		_leaderHost = temp[0];
		_leaderPort = Integer.parseInt(temp[1]);
		
		if ( MyID == _leaderID ) {
			try {
				System.out.println("Open new ServerSocket.");
				InitServer = new ServerSocket(_leaderPort, 5, null );
			} catch (IOException e) {
				System.out.println(e);
				System.out.println("Could not listen on port " + _leaderPort);
				System.exit(-1);
			}
			runHostListen();
			_state = State.DisplayFiles;
		}
		else {	
			try {
				Thread.sleep(1000);			// wait for leader host to set up.
				_socket = new Socket( _leaderHost, _leaderPort);
			} catch (InterruptedException e) {
				displayErrors(e);
			} catch (UnknownHostException e) {
				System.out.println("Unknown host " + _leaderHost);
				displayErrors(e);
			} catch (IOException e) {
				System.out.println("I/O error ");
				displayErrors(e);
			}
			System.out.println("Connection to server established !");
			_state = State.ClientShareFile;			
		}		
	}

	/**
	 * Client sends list of shared files.
	 */
	private void sendFileshare() {

		DataOutputStream _dataOut = null;		
		
		try {
			// input and output streams			
			_dataOut = new DataOutputStream(_socket.getOutputStream());			
		}
		catch (IOException ex) {
			System.out.println("getInput/OutputStream() failed at port " + _hostPort);
		    System.exit(-1);
		}
		
		// Send list of files to leader		
		//
		System.out.println("Sending list of files to leader.");
		try {
			String shareAddr = _myAddr + ":" + _mySharedPort;
			System.out.println("sends " + shareAddr);
			_dataOut.writeUTF(shareAddr);
			for (String file:filelist) {
				System.out.println("sends " + file);
				_dataOut.writeUTF(file);
			}
			_dataOut.flush();
			//_dataOut.close();  // Do NOT close
		} catch (IOException ex) {
			System.out.println("could not send info to init. host.");
			displayErrors(ex);
		}

		_state = State.QueryFile;
	}

	/**
	 * Returns the new leader's info: ID in database.
	 */
	private int hostFindLeader() {
		int minID = Integer.MAX_VALUE;
		int temp;
		while (ConnTable.size() < 3) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				System.out.println(e);
			}
		}
		Enumeration<Integer> IDs = ConnTable.keys();
		while (IDs.hasMoreElements()) {
			temp = IDs.nextElement();
			if (minID > temp) {
				minID = temp;
			}
		}
		return minID;		
	}
	
	

	/**
	 * Runs on init. host. Gathers info from connected nodes. 
	 */
	private void runHostListen() {
		int connectionNum = 0;		// number of connected nodes to init. host.
		
		System.out.println("Listening for connections from clients ....");	
		//
		// listen for connections from clients	
		while (true) {
			
			try {
				HostSide hostSide = new HostSide(InitServer.accept(), this);
				connectionNum++;
				
				Thread th = new Thread(hostSide);
				th.start();				
				
				System.out.println("New Connection has been established." );
			}
			catch (IOException ex) {
				System.out.println("accept() failed at port " + MyPort);
				System.exit(-1);
			}			
			if (connectionNum == 2) {				
				break;
			}
		}	
	}
	
	

	/**
	 * Runs on init. node. Also gets leader info from host.
	 */
	private void runClientWait() {
		
		try {
			_socket = new Socket( _initHost, _hostPort);
			System.out.println("Connection to server established !");
			
			getLeaderInfoFromHost(_socket);			
			
		} catch (UnknownHostException e) {
			System.out.println("Unknown host " + _initHost);
			displayErrors(e);
		} catch (IOException e) {
			System.out.println("I/O error ");
			displayErrors(e);
		}				
	}
	
	/**
	 * Runs on init. node. Waits for leader info from init. host.
	 * @param socket
	 */
	private void getLeaderInfoFromHost(Socket socket) {
		
		DataOutputStream _dataOut = null;
		DataInputStream _dataIn = null;
		
		try {
			// input and output streams
			_dataIn = new DataInputStream(socket.getInputStream());		// deprecated readLine()
			_dataOut = new DataOutputStream(socket.getOutputStream());			
		}
		catch (IOException ex) {
			System.out.println("getInput/OutputStream() failed at port " + _hostPort);
		    System.exit(-1);
		}
				
		// Send ID and Address:Port info to host		
		//
		System.out.println("Sends ID and Address:Port to init host.");
		try {
			_dataOut.writeInt(MyID);
			_dataOut.writeUTF(MyFullAddr);
			_dataOut.flush();
		} catch (IOException ex) {
			System.out.println("could not send info to init. host.");
			displayErrors(ex);
		}
		
		// Wait for leader election result from init. host.
		//					
		// wait till data is available from server
		try {
			while (_dataIn.available() == 0) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					System.out.println(e);
				}													
			}

			/* Host has found leader node. */
			while (_dataIn.available() > 0) {										

				// Reads new leader's info: ID and Addr:Port
				//
				_leaderID = _dataIn.readInt();
				_leaderFullAddr = _dataIn.readUTF();									

				System.out.println("New leader: " + _leaderID + " " + _leaderFullAddr);
				break;
			}
		} catch (IOException ex) {
			System.out.println("could not receive leader's info from init. host.");
			displayErrors(ex);
		}					
	}
	
	/**
	 * Displays connection error and exits program.
	 */
	private void displayErrors(Exception e) {
		
		System.err.println(e);
		System.exit(1);
	}
	

}


/**
 * Runs server-side thread on initial host.
 */
class HostSide implements Runnable {
	static int _createdNum = 0;		// 1=initial host , 2=leader host
	private P2PNode _p2pNode;
	//private int _id;
	private Socket _socket;	
	DataOutputStream _dataOut = null;
	DataInputStream _dataIn = null;
		
	public HostSide(Socket socket, P2PNode p2pNode) {
		_socket = socket;
		_p2pNode = p2pNode;
		_createdNum++;
	}

	@Override
	public void run() {
		
		// receives IP/Port/ID info from initial clients
		/* Get the socket's input and output streams */
		try {
			_dataIn = new DataInputStream(_socket.getInputStream());		// deprecated readLine()
			_dataOut = new DataOutputStream(_socket.getOutputStream());			
		}
		catch (IOException ex) {
			System.out.println("getInput/OutputStream() failed.");
			System.out.println(ex);
		    System.exit(-1);
		}		
		if (_createdNum < 3) {			/* initial host */
			getConnectedNodeInfo();							
			sendLeaderInfo();
		}
		else {							/* leader */
			getFileshare();						
			listenClientQuery();
		}
	}
	

	/**
	 * Receives client's request for file and sends back response.
	 */
	private void listenClientQuery() {

		while (true) {
			try {
				System.out.println("Waiting for file request....");
				while (_dataIn.available() == 0) {
					try {
						Thread.sleep(100);
					} catch (InterruptedException e) {
						System.out.println(e);
					}													
				}

				String fileRequest, addr;
				while (_dataIn.available() > 0) {					
					System.out.println("File request received.");
					fileRequest = _dataIn.readUTF();
					System.out.println("file request: " + fileRequest);
					if (_p2pNode.FileShare.containsKey(fileRequest)) {
						System.out.println("Searching....");
						addr = _p2pNode.FileShare.get(fileRequest);		
						System.out.println("File found.");
						Thread.sleep(50);
						_dataOut.writeBoolean(true);
						_dataOut.writeUTF(fileRequest);
						_dataOut.writeUTF(addr);
						_dataOut.flush();
						System.out.println("Results sent.");
					}
					else {
						_dataOut.writeBoolean(false);
					}
				}
			} catch (IOException ex) {
				System.out.println("Could not read info from connected client.");
				System.out.println(ex);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}		
		}
	}

	/**
	 * Leader gets list of files from each client and puts in database.
	 */
	private void getFileshare() {
		System.out.println("in getFileshare()");
		try {
			while (_dataIn.available() == 0) {
				try {
					Thread.sleep(10);
				} catch (InterruptedException e) {
					System.out.println(e);
				}													
			}

			String filename, fileAddr;
			while (_dataIn.available() > 0) {					

				System.out.println("reading files from client.");
				// Gets list of files from clients.
				//				
				fileAddr = _dataIn.readUTF();
				System.out.println(fileAddr);
				for (int i=0; i<3; i++) {
					filename = _dataIn.readUTF();
					System.out.println(filename);
					_p2pNode.FileShare.put(filename, fileAddr);
				}				
				break;
			}
		} catch (IOException ex) {
			System.out.println("Could not read info from connected client.");
			System.out.println(ex);
		}
	}

	/**
	 * Sends leader ID + Addr:Port to connected client.
	 */
	private void sendLeaderInfo() {
		
		while (!_p2pNode._foundLeader) {
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
				System.out.println(e);
			}
		}
		try {
			int id = _p2pNode._leaderID;
			_dataOut.writeInt(id);
			_dataOut.writeUTF(_p2pNode.ConnTable.get(id));
			_dataOut.flush();
		} catch (IOException e) {
			System.out.println(e);
		}
	}

	/**
	 * Receives ID/IPAddr:Port from client
	 * and puts this in database.
	 */
	private void getConnectedNodeInfo() {
			
		//int connNum = _p2pNode.getConnectedNodes();
		int nodeID;
		String nodeFullAddr;
		
		try {
			while (_dataIn.available() == 0) {
				try {
					Thread.sleep(100);
				} catch (InterruptedException e) {
					System.out.println(e);
				}													
			}

			while (_dataIn.available() > 0) {					

				// Read connected node's ID and full address.
				//
				nodeID = _dataIn.readInt();
				nodeFullAddr = _dataIn.readUTF();
				System.out.println(nodeID + "\t" + nodeFullAddr);
				// Put node info in database
				//
				_p2pNode.ConnTable.put(nodeID, nodeFullAddr);
				break;
			}
		} catch (IOException ex) {
			System.out.println("Could not read info from connected client.");
			System.out.println(ex);
		}			
	}
	
	/*
	 * Called before exit. Cleans up ServerSocket objects.
	 * (non-Javadoc)
	 * @see java.lang.Object#finalize()
	 */
	protected void finalize() {
		/* Clean up*/
		try {
			_dataIn.close();
			_dataOut.close();
			_socket.close();
		}
		
		catch (IOException ex) {
			System.out.println("socket could not close.");
	        System.exit(-1);
		}
	}
}


/**
 * Acts as a file server. Accepts requests for transfering files.
 * @author LN
 *
 */
class P2PServer implements Runnable {
	
	int _port;
	ServerSocket _server;
	//BufferedReader _bufferReaderIn = null;
	DataInputStream _dataIn = null;
		
	public P2PServer(int port) {
		_port = port;
	}

	@Override
	public void run() {
				
		try {			
			// all addresses on localhost
			_server = new ServerSocket(_port, 5, null );
			
			while (true) {
				
				Socket sock = _server.accept();
				String filename="";
				try {
					System.out.println();
					System.out.println("Responding to file transfer request...");
					_dataIn = new DataInputStream(sock.getInputStream());		// deprecated readLine()
								
					while (_dataIn.available() <= 0) {
						try {
							Thread.sleep(100);
						} catch (InterruptedException e) {
							System.out.println(e);
						}													
					}
													
					filename = _dataIn.readUTF();						
					System.out.println("File to send: \"" + filename + "\"");	
															
					File file = new File(filename);												
					FileInputStream fis = new FileInputStream(file);
					
					// output stream to client
					OutputStream socketOutStream = sock.getOutputStream();	
					// Buffer file in byte array
					byte [] bytearray  = new byte [(int)file.length()];												
					
					int chunkSize;
				    while ( (chunkSize = fis.read(bytearray)) != -1 ) {
				    	// Send file to client
					    System.out.println("Transfering file...");
				    	socketOutStream.write(bytearray, 0, chunkSize);				    	
				    }
				    				    
				    socketOutStream.flush();
				    socketOutStream.close();
				    System.out.println("File sent.");
				    fis.close();
				    sock.close();
				}
				catch (IOException ex) {
					System.out.println("getInput/OutputStream() failed.");
				    System.exit(-1);
				}
			}
		}
		catch (IOException ex) {			
			System.out.println(ex.toString());
			System.out.println("Could not listen on port " + _port);
		    System.exit(-1);
			//System.out.println(ex.toString());
		}				
	}
	
	/*
	 * Called before exit. Cleans up ServerSocket objects.
	 * (non-Javadoc)
	 * @see java.lang.Object#finalize()
	 */
	protected void finalize() {
		/* Clean up*/

	}
}
